package com.yeren.socket.tcp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 服务端应用程序
 */
public class Server {
	//运行在服务端的Socket
	private ServerSocket serverSocket;
	//线程池，用于管理客户端连接的交互线程
	private ExecutorService threadPool;
	//保存所有的客户端输出流的集合
	private List<PrintWriter> allOut;
	
	/**
	 * 构造方法用于初始化服务端
	 */
	public Server() throws IOException {
		try {
			/**
			 * 创建ServerSocket时需要制定服务端口
			 */
			System.out.println("初始化服务端");
			serverSocket = new ServerSocket(8888);
			//初始化线程池
			threadPool = Executors.newFixedThreadPool(50);
			//初始化存放所有客户端输出流的集合
			allOut = new ArrayList<PrintWriter>();
			System.out.println("服务器初始化完毕");

		} catch (IOException e) {
			e.printStackTrace();
			throw e;
		}
	}

	public void start() {
		try {
			/**
			 * ServerSocket的accept方法用于监听8888端口，等待客户端的连接
			 * 该方法是一个阻塞方法，直到一个客户端连接，否则该方法一直阻塞。
			 * 若一个客户端连接了，会返回该客户端的Socket
			 */
			while (true) {
				System.out.println("等待客户端连接...");
				Socket socket = serverSocket.accept();
				/**
				 * 当一个客户端连接后，启动一个线程Clienthandler，将该客户端的Socket传入，使得改线程处理与该客户端的交互
				 * 这样我们能再次进入循环，接收下一个客户端的连接了。
				 */
				Runnable handler = new ClientHandler(socket);
				//Thread t=new Thread(handler);
				//t.start();
				/**
				 * 使用线程池分配空闲线程来处理当前连接的客户端
				 */
				threadPool.execute(handler);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	//将给定的输出流存入共享集合
	public synchronized void addOut(PrintWriter pw) {
		allOut.add(pw);
	}
	
	//将给定输出流从共享集合中删除
	public synchronized void removeOut(PrintWriter pw) {
		allOut.remove(pw);
	}

	//将给定的消息转发给所有的客户端
	public synchronized void sendMessage(String message) {
		for (PrintWriter pw : allOut) {
			pw.println(message);
		}
	}

	public static void main(String[] args) {
		Server server;
		try {
			server = new Server();
			server.start();
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("服务器初始化失败");
		}
	}

	/**
	 * 服务端中的一个线程，用于与某个客户端交互，使用线程的目的是使得服务端可以处理多个客户端
	 */
	class ClientHandler implements Runnable {
		private Socket socket;
		private String ip;
		private String nickName;//当前客户端昵称

		//根据给定的客户端的Socket，创建线程体
		public ClientHandler(Socket socket) {
			this.socket = socket;
			/**
			 * 通过socket获取远端的地址信息，对于服务端而言，远端就是客户端了
			 */
			InetAddress address = socket.getInetAddress();
			ip = address.getHostAddress();
			int port = socket.getPort();
			System.out.println(ip + ":" + port + "客户端连接了");
			//改为使用了昵称了，所以不在这里通知了
			//通知其他用户该用户上线了
			//sendMessage("【"+ip+"】上线了");
		}

		/**
		 * 该线程会将当前Socket中的输入流获取用来循环读取客户端发送过来的消息
		 */
		public void run() {
			PrintWriter pw = null;
			//定义在try语句外的目的是，为了在finally中也可以引用到
			try {
				//为了让服务端与客户端发送消息，我们需要通过socket获取输出流
				OutputStream out = socket.getOutputStream();
				OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");
				//创建缓冲字符输出流
				pw = new PrintWriter(osw, true);
				//将客户端的输出流存入共享集合以便使得该客户端也能接收服务端转发的消息
//				allOut.add(pw);
				addOut(pw);
				System.out.println("当前在线人数为：" + allOut.size());
				//通过刚刚连上的客户端的Socket获取输入流，来读取客户端发送过来的信息
				InputStream in = socket.getInputStream();

				InputStreamReader isr = new InputStreamReader(in, "utf-8");
				BufferedReader br = new BufferedReader(isr);
				nickName = br.readLine();
				sendMessage("【" + nickName + "】上线了");
				String message = null;
				//读取客户端发送过来的一行字符串
				/**
				 * 读取客户端发送过来的信息这里windows与linux存在一定的差异：
				 * Linux：当客户端与服务端断开连接后我们通过输入流会读取职到null,但这是不合乎逻辑的，因为缓冲流的readLine()方法若返回null就标示无法通过该流再读到信息。
				 * windows：当客户端与服务端断开连接后readLine()方法会抛出异常
				 */
				while ((message = br.readLine()) != null) {
//					System.out.println("客户端说："+message);
//					pw.println(message);
					/**
					 * 当读取到客户端发送过来的一条消息后，该消息转发给所有客户端
					 */
//					for(PrintWriter o:allOut){
//						o.println(message);
//					}
					sendMessage(nickName + "说：" + message);
				}
			} catch (Exception e) {
				//在windows中的客户端，报错通常是因为断开了连接
			} finally {
				/**
				 * 首先将该客户端的输出流从共享集合中删除
				 */
//				allOut.remove(pw);
				removeOut(pw);
				System.out.println("当前在线人数为：" + allOut.size());
				//通知其他用户，该用户下线了
				sendMessage("【" + nickName + "】下线了");
				/**
				 * 无论是Linux还是windows用户，当服务器断开连接后，我们都应该在服务端也与客户端断开连接
				 */
				try {
					socket.close();
				} catch (IOException e) {

				}
				System.out.println("一个客户端下线了。。。");
			}
		}
	}

}